

package com.junjie.index12306.biz.ticketservice.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.junjie.index12306.biz.ticketservice.common.enums.RefundTypeEnum;
import com.junjie.index12306.biz.ticketservice.common.enums.SourceEnum;
import com.junjie.index12306.biz.ticketservice.common.enums.TicketChainMarkEnum;
import com.junjie.index12306.biz.ticketservice.common.enums.TicketStatusEnum;
import com.junjie.index12306.biz.ticketservice.common.enums.VehicleTypeEnum;
import com.junjie.index12306.biz.ticketservice.dao.entity.StationDO;
import com.junjie.index12306.biz.ticketservice.dao.entity.TicketDO;
import com.junjie.index12306.biz.ticketservice.dao.entity.TrainDO;
import com.junjie.index12306.biz.ticketservice.dao.entity.TrainStationPriceDO;
import com.junjie.index12306.biz.ticketservice.dao.entity.TrainStationRelationDO;
import com.junjie.index12306.biz.ticketservice.dao.mapper.StationMapper;
import com.junjie.index12306.biz.ticketservice.dao.mapper.TicketMapper;
import com.junjie.index12306.biz.ticketservice.dao.mapper.TrainMapper;
import com.junjie.index12306.biz.ticketservice.dao.mapper.TrainStationPriceMapper;
import com.junjie.index12306.biz.ticketservice.dao.mapper.TrainStationRelationMapper;
import com.junjie.index12306.biz.ticketservice.dto.domain.PurchaseTicketPassengerDetailDTO;
import com.junjie.index12306.biz.ticketservice.dto.domain.RouteDTO;
import com.junjie.index12306.biz.ticketservice.dto.domain.SeatClassDTO;
import com.junjie.index12306.biz.ticketservice.dto.domain.TicketListDTO;
import com.junjie.index12306.biz.ticketservice.dto.req.CancelTicketOrderReqDTO;
import com.junjie.index12306.biz.ticketservice.dto.req.PurchaseTicketReqDTO;
import com.junjie.index12306.biz.ticketservice.dto.req.RefundTicketReqDTO;
import com.junjie.index12306.biz.ticketservice.dto.req.TicketOrderItemQueryReqDTO;
import com.junjie.index12306.biz.ticketservice.dto.req.TicketPageQueryReqDTO;
import com.junjie.index12306.biz.ticketservice.dto.resp.RefundTicketRespDTO;
import com.junjie.index12306.biz.ticketservice.dto.resp.TicketOrderDetailRespDTO;
import com.junjie.index12306.biz.ticketservice.dto.resp.TicketPageQueryRespDTO;
import com.junjie.index12306.biz.ticketservice.dto.resp.TicketPurchaseRespDTO;
import com.junjie.index12306.biz.ticketservice.remote.PayRemoteService;
import com.junjie.index12306.biz.ticketservice.remote.TicketOrderRemoteService;
import com.junjie.index12306.biz.ticketservice.remote.dto.PayInfoRespDTO;
import com.junjie.index12306.biz.ticketservice.remote.dto.RefundReqDTO;
import com.junjie.index12306.biz.ticketservice.remote.dto.RefundRespDTO;
import com.junjie.index12306.biz.ticketservice.remote.dto.TicketOrderCreateRemoteReqDTO;
import com.junjie.index12306.biz.ticketservice.remote.dto.TicketOrderItemCreateRemoteReqDTO;
import com.junjie.index12306.biz.ticketservice.remote.dto.TicketOrderPassengerDetailRespDTO;
import com.junjie.index12306.biz.ticketservice.service.SeatService;
import com.junjie.index12306.biz.ticketservice.service.TicketService;
import com.junjie.index12306.biz.ticketservice.service.TrainStationService;
import com.junjie.index12306.biz.ticketservice.service.cache.SeatMarginCacheLoader;
import com.junjie.index12306.biz.ticketservice.service.handler.ticket.dto.TrainPurchaseTicketRespDTO;
import com.junjie.index12306.biz.ticketservice.service.handler.ticket.select.TrainSeatTypeSelector;
import com.junjie.index12306.biz.ticketservice.service.handler.ticket.tokenbucket.TicketAvailabilityTokenBucket;
import com.junjie.index12306.biz.ticketservice.toolkit.DateUtil;
import com.junjie.index12306.biz.ticketservice.toolkit.TimeStringComparator;
import com.junjie.index12306.framework.starter.bases.ApplicationContextHolder;
import com.junjie.index12306.framework.starter.cache.DistributedCache;
import com.junjie.index12306.framework.starter.cache.toolkit.CacheUtil;
import com.junjie.index12306.framework.starter.common.toolkit.BeanUtil;
import com.junjie.index12306.framework.starter.convention.exception.ServiceException;
import com.junjie.index12306.framework.starter.convention.result.Result;
import com.junjie.index12306.framework.starter.designpattern.chain.AbstractChainContext;
import com.junjie.index12306.frameworks.starter.user.core.UserContext;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

import static com.junjie.index12306.biz.ticketservice.common.constant.Index12306Constant.ADVANCE_TICKET_DAY;
import static com.junjie.index12306.biz.ticketservice.common.constant.RedisKeyConstant.LOCK_PURCHASE_TICKETS;
import static com.junjie.index12306.biz.ticketservice.common.constant.RedisKeyConstant.LOCK_PURCHASE_TICKETS_V2;
import static com.junjie.index12306.biz.ticketservice.common.constant.RedisKeyConstant.LOCK_REGION_TRAIN_STATION;
import static com.junjie.index12306.biz.ticketservice.common.constant.RedisKeyConstant.LOCK_REGION_TRAIN_STATION_MAPPING;
import static com.junjie.index12306.biz.ticketservice.common.constant.RedisKeyConstant.REGION_TRAIN_STATION;
import static com.junjie.index12306.biz.ticketservice.common.constant.RedisKeyConstant.REGION_TRAIN_STATION_MAPPING;
import static com.junjie.index12306.biz.ticketservice.common.constant.RedisKeyConstant.TRAIN_INFO;
import static com.junjie.index12306.biz.ticketservice.common.constant.RedisKeyConstant.TRAIN_STATION_PRICE;
import static com.junjie.index12306.biz.ticketservice.common.constant.RedisKeyConstant.TRAIN_STATION_REMAINING_TICKET;
import static com.junjie.index12306.biz.ticketservice.toolkit.DateUtil.convertDateToLocalTime;

/**
 * 车票接口实现
 *
 *
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class TicketServiceImpl extends ServiceImpl<TicketMapper, TicketDO> implements TicketService, CommandLineRunner {

    private final TrainMapper trainMapper;
    private final TrainStationRelationMapper trainStationRelationMapper;
    private final TrainStationPriceMapper trainStationPriceMapper;
    private final DistributedCache distributedCache;
    private final TicketOrderRemoteService ticketOrderRemoteService;
    private final PayRemoteService payRemoteService;
    private final StationMapper stationMapper;
    private final SeatService seatService;
    private final TrainStationService trainStationService;
    private final TrainSeatTypeSelector trainSeatTypeSelector;
    private final SeatMarginCacheLoader seatMarginCacheLoader;
    private final AbstractChainContext<TicketPageQueryReqDTO> ticketPageQueryAbstractChainContext;
    private final AbstractChainContext<PurchaseTicketReqDTO> purchaseTicketAbstractChainContext;
    private final AbstractChainContext<RefundTicketReqDTO> refundReqDTOAbstractChainContext;
    private final RedissonClient redissonClient;
    private final ConfigurableEnvironment environment;
    private final TicketAvailabilityTokenBucket ticketAvailabilityTokenBucket;
    private TicketService ticketService;

    @Value("${ticket.availability.cache-update.type:}")
    private String ticketAvailabilityCacheUpdateType;
    @Value("${framework.cache.redis.prefix:}")
    private String cacheRedisPrefix;

    // 这里是每个第一次查询才会加载到缓存
    @Override
    public TicketPageQueryRespDTO pageListTicketQueryV1(TicketPageQueryReqDTO requestParam) {
        // 责任链模式 验证城市名称是否存在、不存在加载缓存以及出发日期不能小于当前日期等等
        ticketPageQueryAbstractChainContext.handler(TicketChainMarkEnum.TRAIN_QUERY_FILTER.name(), requestParam);
        StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) distributedCache.getInstance();
        // v1 版本存在严重的性能深渊问题，v2 版本完美的解决了该问题。通过 Jmeter 压测聚合报告得知，性能提升在 300% - 500%+
        // 1、根据出发地和终点地 从缓存中获取 地区与站点映射 关系 比如 BJP 对应 北京 NJH 对应 南京
        List<Object> stationDetails = stringRedisTemplate.opsForHash()
                .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation()));
        long count = stationDetails.stream().filter(Objects::isNull).count();
        if (count > 0) {
            // 开启站点查询分布式锁 Key
            RLock lock = redissonClient.getLock(LOCK_REGION_TRAIN_STATION_MAPPING);
            lock.lock();
            try {
                // 双重判定
                stationDetails = stringRedisTemplate.opsForHash()
                        .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation()));
                count = stationDetails.stream().filter(Objects::isNull).count();
                if (count > 0) {
                    // 从数据库查询出车站信息
                    List<StationDO> stationDOList = stationMapper.selectList(Wrappers.emptyWrapper());
                    Map<String, String> regionTrainStationMap = new HashMap<>();
                    stationDOList.forEach(each -> regionTrainStationMap.put(each.getCode(), each.getRegionName()));
                    // 存储地区与站点映射的缓存进Redis
                    stringRedisTemplate.opsForHash().putAll(REGION_TRAIN_STATION_MAPPING, regionTrainStationMap);
                    stationDetails = new ArrayList<>();
                    stationDetails.add(regionTrainStationMap.get(requestParam.getFromStation()));
                    stationDetails.add(regionTrainStationMap.get(requestParam.getToStation()));
                }
            } finally {
                lock.unlock();
            }
        }
        // 车次集合
        List<TicketListDTO> seatResults = new ArrayList<>();
        // 根据起点城市和终点城市构建key
        String buildRegionTrainStationHashKey = String.format(REGION_TRAIN_STATION, stationDetails.get(0), stationDetails.get(1));
        // 2、获取初始站到终点站所有站点的信息
        // key:车站ID  val:列车详细信息
        Map<Object, Object> regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey);
        if (MapUtil.isEmpty(regionTrainStationAllMap)) { // 缓存为空就加载一下
            RLock lock = redissonClient.getLock(LOCK_REGION_TRAIN_STATION);
            lock.lock();
            try {
                regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey);
                // 双重判定
                if (MapUtil.isEmpty(regionTrainStationAllMap)) {
                    // 查询 出发地到目的地 列车站点 关系集合
                    LambdaQueryWrapper<TrainStationRelationDO> queryWrapper = Wrappers.lambdaQuery(TrainStationRelationDO.class)
                            .eq(TrainStationRelationDO::getStartRegion, stationDetails.get(0))
                            .eq(TrainStationRelationDO::getEndRegion, stationDetails.get(1));
                    List<TrainStationRelationDO> trainStationRelationList = trainStationRelationMapper.selectList(queryWrapper);
                    for (TrainStationRelationDO each : trainStationRelationList) {
                        // 从缓存中获取车站信息 车站都是缓存15天
                        TrainDO trainDO = distributedCache.safeGet(
                                TRAIN_INFO + each.getTrainId(),
                                TrainDO.class,
                                () -> trainMapper.selectById(each.getTrainId()),
                                ADVANCE_TICKET_DAY,
                                TimeUnit.DAYS);
                        TicketListDTO result = new TicketListDTO();
                        result.setTrainId(String.valueOf(trainDO.getId()));
                        result.setTrainNumber(trainDO.getTrainNumber());
                        result.setDepartureTime(convertDateToLocalTime(each.getDepartureTime(), "HH:mm"));
                        result.setArrivalTime(convertDateToLocalTime(each.getArrivalTime(), "HH:mm"));
                        result.setDuration(DateUtil.calculateHourDifference(each.getDepartureTime(), each.getArrivalTime()));
                        result.setDeparture(each.getDeparture());
                        result.setArrival(each.getArrival());
                        result.setDepartureFlag(each.getDepartureFlag());
                        result.setArrivalFlag(each.getArrivalFlag());
                        result.setTrainType(trainDO.getTrainType());
                        result.setTrainBrand(trainDO.getTrainBrand());
                        if (StrUtil.isNotBlank(trainDO.getTrainTag())) {
                            result.setTrainTags(StrUtil.split(trainDO.getTrainTag(), ","));
                        }
                        // 计算到达天数 出发时间 至 到达时间
                        long betweenDay = cn.hutool.core.date.DateUtil.betweenDay(each.getDepartureTime(), each.getArrivalTime(), false);
                        result.setDaysArrived((int) betweenDay);
                        // 销售状态就看当前时间有没有超过销售时间
                        result.setSaleStatus(new Date().after(trainDO.getSaleTime()) ? 0 : 1);
                        result.setSaleTime(convertDateToLocalTime(trainDO.getSaleTime(), "MM-dd HH:mm"));
                        seatResults.add(result);
                        regionTrainStationAllMap.put(CacheUtil.buildKey(String.valueOf(each.getTrainId()), each.getDeparture(), each.getArrival()), JSON.toJSONString(result));
                    }
                    stringRedisTemplate.opsForHash().putAll(buildRegionTrainStationHashKey, regionTrainStationAllMap);
                }
            } finally {
                lock.unlock();
            }
        }
        seatResults = CollUtil.isEmpty(seatResults)
                ? regionTrainStationAllMap.values().stream().map(each -> JSON.parseObject(each.toString(), TicketListDTO.class)).toList()
                : seatResults;
        // 自定义时间比较器，根据出发时间进行排序
        seatResults = seatResults.stream().sorted(new TimeStringComparator()).toList();
        // keypoint 这里的性能问题就在这，循环去Redis挨个查，像v2接口用管道就特别快，节省网络开销
        // 3、给每个车次填充上座位数据包括座位价格
        for (TicketListDTO each : seatResults) {
            // 从缓存中查询 当前车次所有种类座位 的价格 json 比如 当前车次 一等座10元 二等座5元
            String trainStationPriceStr = distributedCache.safeGet(
                    String.format(TRAIN_STATION_PRICE, each.getTrainId(), each.getDeparture(), each.getArrival()),
                    String.class,
                    () -> {
                        LambdaQueryWrapper<TrainStationPriceDO> trainStationPriceQueryWrapper = Wrappers.lambdaQuery(TrainStationPriceDO.class)
                                .eq(TrainStationPriceDO::getDeparture, each.getDeparture())
                                .eq(TrainStationPriceDO::getArrival, each.getArrival())
                                .eq(TrainStationPriceDO::getTrainId, each.getTrainId());
                        return JSON.toJSONString(trainStationPriceMapper.selectList(trainStationPriceQueryWrapper));
                    },
                    ADVANCE_TICKET_DAY,
                    TimeUnit.DAYS
            );
            // json转换成集合
            List<TrainStationPriceDO> trainStationPriceDOList = JSON.parseArray(trainStationPriceStr, TrainStationPriceDO.class);
            List<SeatClassDTO> seatClassList = new ArrayList<>();
            // 遍历 当前车次所有种类座位信息
            trainStationPriceDOList.forEach(item -> {
                // 获取座位类型
                String seatType = String.valueOf(item.getSeatType());
                String keySuffix = StrUtil.join("_", each.getTrainId(), item.getDeparture(), item.getArrival());
                // 获取当前车次，当前座位类型，剩下还有多少个座位
                Object quantityObj = stringRedisTemplate.opsForHash().get(TRAIN_STATION_REMAINING_TICKET + keySuffix, seatType);
                int quantity = Optional.ofNullable(quantityObj)
                        .map(Object::toString)
                        .map(Integer::parseInt)
                        .orElseGet(() -> {
                            // 如果获取出来为空 就进行座位余量缓存加载 并返回座位余量
                            Map<String, String> seatMarginMap = seatMarginCacheLoader.load(String.valueOf(each.getTrainId()), seatType, item.getDeparture(), item.getArrival());
                            return Optional.ofNullable(seatMarginMap.get(String.valueOf(item.getSeatType()))).map(Integer::parseInt).orElse(0);
                        });
                // 放入该座位类型的 集合中
                seatClassList.add(new SeatClassDTO(item.getSeatType(), quantity, new BigDecimal(item.getPrice()).divide(new BigDecimal("100"), 1, RoundingMode.HALF_UP), false));
            });
            // 给当前循环中的车次装填座位信息
            each.setSeatClassList(seatClassList);
        }
        return TicketPageQueryRespDTO.builder()
                .trainList(seatResults)
                .departureStationList(buildDepartureStationList(seatResults))
                .arrivalStationList(buildArrivalStationList(seatResults))
                .trainBrandList(buildTrainBrandList(seatResults))
                .seatClassTypeList(buildSeatClassList(seatResults))
                .build();
    }

    /**
     * 核心就是使用Redis管道进行查询接口性能优化
     * https://blog.csdn.net/m0_51935008/article/details/133252868
     */
    @Override
    public TicketPageQueryRespDTO pageListTicketQueryV2(TicketPageQueryReqDTO requestParam) {
        // 责任链模式 验证城市名称是否存在、不存在加载缓存以及出发日期不能小于当前日期等等
        ticketPageQueryAbstractChainContext.handler(TicketChainMarkEnum.TRAIN_QUERY_FILTER.name(), requestParam);
        StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) distributedCache.getInstance();
        // v2 版本更符合企业级高并发真实场景解决方案，完美解决了 v1 版本性能深渊问题。通过 Jmeter 压测聚合报告得知，性能提升在 300% - 500%+
        // 其实还能有 v3 版本，性能估计在原基础上还能进一步提升一倍。不过 v3 版本太过于复杂，不易读且不易扩展，就不写具体的代码了。面试中 v2 版本已经够和面试官吹的了
        // 1、根据出发地和终点地 从缓存中获取 地区与站点映射 关系 比如 BJP 对应 北京 NJH 对应 南京
        List<Object> stationDetails = stringRedisTemplate.opsForHash()
                .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation()));
        String buildRegionTrainStationHashKey = String.format(REGION_TRAIN_STATION, stationDetails.get(0), stationDetails.get(1));
        // 2、车次查询 比如北京到杭州要经过四个站
        Map<Object, Object> regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey);
        List<TicketListDTO> seatResults = regionTrainStationAllMap.values().stream()
                .map(each -> JSON.parseObject(each.toString(), TicketListDTO.class))
                .sorted(new TimeStringComparator())
                .toList();
        // 构建出 查询价格 keys集合
        List<String> trainStationPriceKeys = seatResults.stream()
                .map(each -> String.format(cacheRedisPrefix + TRAIN_STATION_PRICE, each.getTrainId(), each.getDeparture(), each.getArrival()))
                .toList();
        // 3、redis批量操作 查询出 各个站点不同座位价格
        List<Object> trainStationPriceObjs = stringRedisTemplate.executePipelined((RedisCallback<String>) connection -> {
            trainStationPriceKeys.forEach(each -> connection.stringCommands().get(each.getBytes()));
            return null;
        });
        // 每个站点之间的每个不同座位的价格信息
        List<TrainStationPriceDO> trainStationPriceDOList = new ArrayList<>();
        // 余票keys集合
        List<String> trainStationRemainingKeyList = new ArrayList<>();
        // 4、在将redis取出来的不同座位价格信息 转换成实体类的同时 收集余票keys集合!
        for (Object each : trainStationPriceObjs) {
            // 将 北京到南京各个座位的不同price数据 转成具体的类型集合
            List<TrainStationPriceDO> trainStationPriceList = JSON.parseArray(each.toString(), TrainStationPriceDO.class);
            trainStationPriceDOList.addAll(trainStationPriceList);
            for (TrainStationPriceDO item : trainStationPriceList) {
                String trainStationRemainingKey = cacheRedisPrefix + TRAIN_STATION_REMAINING_TICKET + StrUtil.join("_", item.getTrainId(), item.getDeparture(), item.getArrival());
                trainStationRemainingKeyList.add(trainStationRemainingKey);
            }
        }
        // 5、根据余票keys管道批量查询出余票信息
        List<Object> TrainStationRemainingObjs = stringRedisTemplate.executePipelined((RedisCallback<String>) connection -> {
            for (int i = 0; i < trainStationRemainingKeyList.size(); i++) {
                connection.hashCommands().hGet(trainStationRemainingKeyList.get(i).getBytes(), trainStationPriceDOList.get(i).getSeatType().toString().getBytes());
            }
            return null;
        });
        // 6、挨个设置座位信息
        for (TicketListDTO each : seatResults) {
            // 当前列车座位类型集合
            List<Integer> seatTypesByCode = VehicleTypeEnum.findSeatTypesByCode(each.getTrainType());
            // 当前列车余票集合
            List<Object> remainingTicket = new ArrayList<>(TrainStationRemainingObjs.subList(0, seatTypesByCode.size()));
            // 当前列车价格集合
            List<TrainStationPriceDO> trainStationPriceDOSub = new ArrayList<>(trainStationPriceDOList.subList(0, seatTypesByCode.size()));
            // clear方法会清除容器中所有元素 因此需要提前备份
            TrainStationRemainingObjs.subList(0, seatTypesByCode.size()).clear();
            trainStationPriceDOList.subList(0, seatTypesByCode.size()).clear();
            List<SeatClassDTO> seatClassList = new ArrayList<>();
            for (int i = 0; i < trainStationPriceDOSub.size(); i++) {
                TrainStationPriceDO trainStationPriceDO = trainStationPriceDOSub.get(i);
                SeatClassDTO seatClassDTO = SeatClassDTO.builder()
                        .type(trainStationPriceDO.getSeatType())
                        .quantity(Integer.parseInt(remainingTicket.get(i).toString()))
                        .price(new BigDecimal(trainStationPriceDO.getPrice()).divide(new BigDecimal("100"), 1, RoundingMode.HALF_UP))
                        .candidate(false)
                        .build();
                seatClassList.add(seatClassDTO);
            }
            each.setSeatClassList(seatClassList);
        }
        return TicketPageQueryRespDTO.builder()
                .trainList(seatResults)
                .departureStationList(buildDepartureStationList(seatResults))
                .arrivalStationList(buildArrivalStationList(seatResults))
                .trainBrandList(buildTrainBrandList(seatResults))
                .seatClassTypeList(buildSeatClassList(seatResults))
                .build();
    }


    @Override
    public TicketPurchaseRespDTO purchaseTicketsV1(PurchaseTicketReqDTO requestParam) {
        // 责任链模式，验证 1：参数必填 2：参数正确性 3：乘客是否已买当前车次等...
        purchaseTicketAbstractChainContext.handler(TicketChainMarkEnum.TRAIN_PURCHASE_TICKET_FILTER.name(), requestParam);
        // v1 版本购票存在 4 个较为严重的问题，v2 版本相比较 v1 版本更具有业务特点以及性能，整体提升较大
        String lockKey = environment.resolvePlaceholders(String.format(LOCK_PURCHASE_TICKETS, requestParam.getTrainId()));
        // 可以优化成公平锁，会减少用户响应时间
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        try {
            // 分布式锁，锁住整个购票流程
            return ticketService.executePurchaseTickets(requestParam);
        } finally {
            lock.unlock();
        }
    }

    // Caffeine 一个列车的本地公平锁创建一天后失效
    private final Cache<String, ReentrantLock> localLockMap = Caffeine.newBuilder()
            .expireAfterWrite(1, TimeUnit.DAYS)
            .build();

    /**
     * 按照 v1 架构的设计存在以下问题：大量请求会因为分布式锁的申请而发生阻塞，导致请求无法快速处理。
     * 这会导致后续请求长时间被阻塞，使系统陷入假死状态。无论请求的数量有多大，系统都无法返回响应。
     * 此外，随着请求的积累，还存在内存溢出的风险。
     * 更糟糕的是，如果 SpringBoot Tomcat 的线程池被分布式锁占用，查询请求也将无法得到响应。
     * 默认 Redisson 的获取锁是非公平锁，这个概念和 ReentrantLock 中的非公平锁概念是一致的。
     * 非公平锁（Non-Fair Lock）是一种锁的获取策略，它允许在锁释放时，新的请求可以插队获取锁，而不必按照请求的顺序等待。相对而言，公平锁（Fair Lock）则按照请求的顺序分配锁，确保较早请求的线程先获得锁。
     * 使用非公平锁可以带来一些性能上的优势，例如减少线程切换和提高吞吐量。然而，非公平锁也存在一些问题：
     * 1. 饥饿问题（Starvation）：由于非公平锁允许新的请求插队获取锁，较早的请求可能会被后续请求一直抢占，导致某些线程长时间无法获取锁，导致饥饿问题。这可能会影响系统的公平性和可预测性。
     * 2. 公平性问题：非公平锁的获取顺序不遵循请求的先后顺序，因此无法保证所有线程都能在合理的时间内获得锁，可能导致某些线程长时间等待。
     * 为了避免这种情况，我们需要按照公平锁的方式请求和释放锁，使用后的场景就是释放锁后再获取锁要严格按照请求分布式锁的先后顺序执行。
     *
     * 问题来了，假如一趟列车有几十万人抢票，但是真正能购票的用户可能也就几千人。也就意味着哪怕几十万人都去请求这个分布式锁，最终也就几十万人中的几千人是有效的，其它都是无效获取分布式锁的行为。
     * 那这块的分布式锁逻辑是不是可以优化呢？比如不让所有抢购列车的用户去申请分布式锁，而是让少量用户去请求获取分布式锁。这样优化的话，可以极大情况节省 Redis 申请分布式锁的开销压力。
     *
     * 咱们以高铁复兴号举例：一趟列车有三种座位，分别是商务座、一等座和二等座。v1 版本购票接口的分布式锁锁定规则是锁定整个列车，也就是列车的唯一键列车 ID。相当于同一时间一趟列车只允许单个用户进行分配座位、创建订单等流程。
     * 假设 A 用户购买二等座、B 用户购买一等座以及 C 用户购买商务座，按照现有流程这三个用户的请求是串行的，但从真实场景以及数据一致性分析，这三个用户是不是也能并行？三个用户对应三个不同的座位，互不影响，完全可以并行。这样可以极大提升系统出票的能力，个别场景下，系统的处理能力可以提升 300%。
     * 当然，事情往往不会进展的如此顺利。如果 A 用户购买了二等座和一等座，B 用户购买了一等座和商务座，这就涉及到一个锁重合互斥问题。
     *
     * 加入本地锁后，全局的购票顺序将变为局部顺序。当购票人数非常多时，为了减轻缓存的压力，可以适当牺牲一定的全局有序性。
     * 在购票人数较多且缓存压力较大时，本地锁和分布式锁的组合可以在一定程度上平衡性能和购票顺序。而在购票人数较少且重视购票顺序的情况下，使用分布式锁可以确保全局有序性。
     *
     * 其实有了令牌桶限流后，到达redis抢锁的压力已经很小了，加不加本地锁都行，不过有本地锁会进一步减小进入到缓存的压力
     *
     * keypoint 调整锁的粒度 当用户为多个乘车人购票选择了多个不同座位类型时，我们可以加多把锁，锁全部获取到才可以执行购票流程。
     * @param requestParam 车票购买请求参数
     */
    @Override
    public TicketPurchaseRespDTO purchaseTicketsV2(PurchaseTicketReqDTO requestParam) {
        // 1、责任链模式，验证 1：参数必填 2：参数正确性 3：乘客是否已买当前车次等...
        purchaseTicketAbstractChainContext.handler(TicketChainMarkEnum.TRAIN_PURCHASE_TICKET_FILTER.name(), requestParam);
        // 2、从令牌桶获取令牌
        boolean tokenResult = ticketAvailabilityTokenBucket.takeTokenFromBucket(requestParam);
        if (!tokenResult) {
            throw new ServiceException("列车站点已无余票");
        }
        // v1 版本购票存在 4 个较为严重的问题，v2 版本相比较 v1 版本更具有业务特点以及性能，整体提升较大
        // 3、创建 本次请求需要获取的本地锁的集合 本次请求需要获取的分布式锁的集合 按照座位类型进行分组
        List<ReentrantLock> localLockList = new ArrayList<>();
        List<RLock> distributedLockList = new ArrayList<>();
        Map<Integer, List<PurchaseTicketPassengerDetailDTO>> seatTypeMap = requestParam.getPassengers().stream()
                .collect(Collectors.groupingBy(PurchaseTicketPassengerDetailDTO::getSeatType));
        // 4、遍历座位类型分组后的集合 主要目的是 填充本地锁和分布式锁集合 具体原因参考方法上的keypoint
        seatTypeMap.forEach((searType, count) -> {
            // 5、构建key（trainId+座位类型），获取Caffeine里面的本地公平锁
            String lockKey = environment.resolvePlaceholders(String.format(LOCK_PURCHASE_TICKETS_V2, requestParam.getTrainId(), searType));
            ReentrantLock localLock = localLockMap.getIfPresent(lockKey);
            if (localLock == null) {
                // 5.1、不存在的话执行加载流程
                // Caffeine 不像 ConcurrentHashMap 做了并发读写安全控制，这里需要咱们自己控制
                synchronized (TicketService.class) {
                    // 双重判定的方式，避免重复创建
                    if ((localLock = localLockMap.getIfPresent(lockKey)) == null) {
                        // 5.2、创建本地公平锁并放入本地公平锁容器中
                        localLock = new ReentrantLock(true);
                        localLockMap.put(lockKey, localLock);
                    }
                }
            }
            // 5.3、添加到本地锁集合与分布式锁集合
            localLockList.add(localLock);
            RLock distributedLock = redissonClient.getFairLock(lockKey);
            distributedLockList.add(distributedLock);
        });
        try {
            // 6、循环请求本地锁
            localLockList.forEach(ReentrantLock::lock);
            // 7、循环请求分布式锁
            distributedLockList.forEach(RLock::lock);
            // 8、执行购票流程
            return ticketService.executePurchaseTickets(requestParam);
        } finally {
            // 9、释放本地锁
            localLockList.forEach(localLock -> {
                try {
                    localLock.unlock();
                } catch (Throwable ignored) {
                }
            });
            // 10、释放分布式锁
            distributedLockList.forEach(distributedLock -> {
                try {
                    distributedLock.unlock();
                } catch (Throwable ignored) {
                }
            });
        }
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public TicketPurchaseRespDTO executePurchaseTickets(PurchaseTicketReqDTO requestParam) {
        // 车票订单详情返回参数
        List<TicketOrderDetailRespDTO> ticketOrderDetailResults = new ArrayList<>();
        String trainId = requestParam.getTrainId();
        // 节假日高并发购票Redis能扛得住么？ 
        // 1、获取车次信息
        TrainDO trainDO = distributedCache.safeGet(
                TRAIN_INFO + trainId,
                TrainDO.class,
                () -> trainMapper.selectById(trainId),
                ADVANCE_TICKET_DAY,
                TimeUnit.DAYS);
        // 2、列车座位选择器 获取选出来的座位结果 并转成车票实体集合
        List<TrainPurchaseTicketRespDTO> trainPurchaseTicketResults = trainSeatTypeSelector.select(trainDO.getTrainType(), requestParam);
        List<TicketDO> ticketDOList = trainPurchaseTicketResults.stream()
                .map(each -> TicketDO.builder()
                        .username(UserContext.getUsername())
                        .trainId(Long.parseLong(requestParam.getTrainId()))
                        .carriageNumber(each.getCarriageNumber())
                        .seatNumber(each.getSeatNumber())
                        .passengerId(each.getPassengerId())
                        .ticketStatus(TicketStatusEnum.UNPAID.getCode())
                        .build())
                .toList();
        // 3、批量保存车票
        saveBatch(ticketDOList);
        Result<String> ticketOrderResult;
        try {
            // 车票订单详情创建请求参数
            List<TicketOrderItemCreateRemoteReqDTO> orderItemCreateRemoteReqDTOList = new ArrayList<>();
            // 4、订单创建流程 遍历选出来的座位集合 去创建订单对象 并远程调用订单服务创建对应的订单
            trainPurchaseTicketResults.forEach(each -> {
                TicketOrderItemCreateRemoteReqDTO orderItemCreateRemoteReqDTO = TicketOrderItemCreateRemoteReqDTO.builder()
                        .amount(each.getAmount())
                        .carriageNumber(each.getCarriageNumber())
                        .seatNumber(each.getSeatNumber())
                        .idCard(each.getIdCard())
                        .idType(each.getIdType())
                        .phone(each.getPhone())
                        .seatType(each.getSeatType())
                        .ticketType(each.getUserType())
                        .realName(each.getRealName())
                        .build();
                TicketOrderDetailRespDTO ticketOrderDetailRespDTO = TicketOrderDetailRespDTO.builder()
                        .amount(each.getAmount())
                        .carriageNumber(each.getCarriageNumber())
                        .seatNumber(each.getSeatNumber())
                        .idCard(each.getIdCard())
                        .idType(each.getIdType())
                        .seatType(each.getSeatType())
                        .ticketType(each.getUserType())
                        .realName(each.getRealName())
                        .build();
                orderItemCreateRemoteReqDTOList.add(orderItemCreateRemoteReqDTO);
                ticketOrderDetailResults.add(ticketOrderDetailRespDTO);
            });
            // 4.1、查询列车站点关系集合
            LambdaQueryWrapper<TrainStationRelationDO> queryWrapper = Wrappers.lambdaQuery(TrainStationRelationDO.class)
                    .eq(TrainStationRelationDO::getTrainId, trainId)
                    .eq(TrainStationRelationDO::getDeparture, requestParam.getDeparture())
                    .eq(TrainStationRelationDO::getArrival, requestParam.getArrival());
            // 4.2、构建主订单，并把 上面查询出来的子订单 塞入主订单中
            TrainStationRelationDO trainStationRelationDO = trainStationRelationMapper.selectOne(queryWrapper);
            TicketOrderCreateRemoteReqDTO orderCreateRemoteReqDTO = TicketOrderCreateRemoteReqDTO.builder()
                    .departure(requestParam.getDeparture())
                    .arrival(requestParam.getArrival())
                    .orderTime(new Date())
                    .source(SourceEnum.INTERNET.getCode())
                    .trainNumber(trainDO.getTrainNumber())
                    .departureTime(trainStationRelationDO.getDepartureTime())
                    .arrivalTime(trainStationRelationDO.getArrivalTime())
                    .ridingDate(trainStationRelationDO.getDepartureTime())
                    .userId(UserContext.getUserId())
                    .username(UserContext.getUsername())
                    .trainId(Long.parseLong(requestParam.getTrainId()))
                    .ticketOrderItems(orderItemCreateRemoteReqDTOList)
                    .build();
            // 4.3、调用订单服务创建订单
            ticketOrderResult = ticketOrderRemoteService.createTicketOrder(orderCreateRemoteReqDTO);
            if (!ticketOrderResult.isSuccess() || StrUtil.isBlank(ticketOrderResult.getData())) {
                log.error("订单服务调用失败，返回结果：{}", ticketOrderResult.getMessage());
                throw new ServiceException("订单服务调用失败");
            }
        } catch (Throwable ex) {
            log.error("远程调用订单服务创建错误，请求参数：{}", JSON.toJSONString(requestParam), ex);
            throw ex;
        }
        return new TicketPurchaseRespDTO(ticketOrderResult.getData(), ticketOrderDetailResults);
    }

    @Override
    public PayInfoRespDTO getPayInfo(String orderSn) {
        return payRemoteService.getPayInfo(orderSn).getData();
    }

    @Override
    public void cancelTicketOrder(CancelTicketOrderReqDTO requestParam) {
        // 取消订单 这里逻辑跟MQ消费者那边逻辑差不多
        // 1、远程调用订单服务关闭订单
        Result<Void> cancelOrderResult = ticketOrderRemoteService.cancelTicketOrder(requestParam);
        // 2、如果关闭订单成功，且不为cannel形式处理的话 做后置处理
        if (cancelOrderResult.isSuccess() && !StrUtil.equals(ticketAvailabilityCacheUpdateType, "binlog")) {
            // 3、远程调用订单服务根据订单号查询主订单
            Result<com.junjie.index12306.biz.ticketservice.remote.dto.TicketOrderDetailRespDTO> ticketOrderDetailResult = ticketOrderRemoteService.queryTicketOrderByOrderSn(requestParam.getOrderSn());
            com.junjie.index12306.biz.ticketservice.remote.dto.TicketOrderDetailRespDTO ticketOrderDetail = ticketOrderDetailResult.getData();
            String trainId = String.valueOf(ticketOrderDetail.getTrainId());
            String departure = ticketOrderDetail.getDeparture();
            String arrival = ticketOrderDetail.getArrival();
            List<TicketOrderPassengerDetailRespDTO> trainPurchaseTicketResults = ticketOrderDetail.getPassengerDetails();
            try {
                // 4、解锁选中以及沿途车票状态
                seatService.unlock(trainId, departure, arrival, BeanUtil.convert(trainPurchaseTicketResults, TrainPurchaseTicketRespDTO.class));
            } catch (Throwable ex) {
                log.error("[取消订单] 订单号：{} 回滚列车DB座位状态失败", requestParam.getOrderSn(), ex);
                throw ex;
            }
            // 5、令牌桶回滚
            ticketAvailabilityTokenBucket.rollbackInBucket(ticketOrderDetail);
            try {
                // 6、缓存座位归还
                StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) distributedCache.getInstance();
                // 6.1、子订单列表根据座位分组
                Map<Integer, List<TicketOrderPassengerDetailRespDTO>> seatTypeMap = trainPurchaseTicketResults.stream()
                        .collect(Collectors.groupingBy(TicketOrderPassengerDetailRespDTO::getSeatType));
                // 6.2、查询出站点路线
                List<RouteDTO> routeDTOList = trainStationService.listTakeoutTrainStationRoute(trainId, departure, arrival);
                // 6.3、循环站点路线以及分组后的子订单，每个站点的对应座位都要归还
                routeDTOList.forEach(each -> {
                    String keySuffix = StrUtil.join("_", trainId, each.getStartStation(), each.getEndStation());
                    seatTypeMap.forEach((seatType, ticketOrderPassengerDetailRespDTOList) -> {
                        stringRedisTemplate.opsForHash()
                                .increment(TRAIN_STATION_REMAINING_TICKET + keySuffix, String.valueOf(seatType), ticketOrderPassengerDetailRespDTOList.size());
                    });
                });
            } catch (Throwable ex) {
                log.error("[取消关闭订单] 订单号：{} 回滚列车Cache余票失败", requestParam.getOrderSn(), ex);
                throw ex;
            }
        }
    }

    @Override
    public RefundTicketRespDTO commonTicketRefund(RefundTicketReqDTO requestParam) {
        // 1、责任链模式，验证 1：参数必填（订单号、退款类型、部分退款子订单记录集合）
        refundReqDTOAbstractChainContext.handler(TicketChainMarkEnum.TRAIN_REFUND_TICKET_FILTER.name(), requestParam);
        // 2、远程调用订单服务 根据订单号查询车票订单 以及 车票子订单 是否存在
        Result<com.junjie.index12306.biz.ticketservice.remote.dto.TicketOrderDetailRespDTO> orderDetailRespDTOResult = ticketOrderRemoteService.queryTicketOrderByOrderSn(requestParam.getOrderSn());
        if (!orderDetailRespDTOResult.isSuccess() && Objects.isNull(orderDetailRespDTOResult.getData())) {
            throw new ServiceException("车票订单不存在");
        }
        com.junjie.index12306.biz.ticketservice.remote.dto.TicketOrderDetailRespDTO ticketOrderDetailRespDTO = orderDetailRespDTOResult.getData();
        List<TicketOrderPassengerDetailRespDTO> passengerDetails = ticketOrderDetailRespDTO.getPassengerDetails();
        if (CollectionUtil.isEmpty(passengerDetails)) {
            throw new ServiceException("车票子订单不存在");
        }
        RefundReqDTO refundReqDTO = new RefundReqDTO();
        if (RefundTypeEnum.PARTIAL_REFUND.getType().equals(requestParam.getType())) {
            // 3.1、 部分退款
            TicketOrderItemQueryReqDTO ticketOrderItemQueryReqDTO = new TicketOrderItemQueryReqDTO();
            ticketOrderItemQueryReqDTO.setOrderSn(requestParam.getOrderSn());
            ticketOrderItemQueryReqDTO.setOrderItemRecordIds(requestParam.getSubOrderRecordIdReqList());
            // 3.2、根据前端传递的ids，调用远程订单服务去查询出子订单
            Result<List<TicketOrderPassengerDetailRespDTO>> queryTicketItemOrderById = ticketOrderRemoteService.queryTicketItemOrderById(ticketOrderItemQueryReqDTO);
            // 3.3、核实前端传递的 要退款的订单 是否为真实的订单 如果不是真实的订单id就要过滤掉
            List<TicketOrderPassengerDetailRespDTO> partialRefundPassengerDetails = passengerDetails.stream()
                    .filter(item -> queryTicketItemOrderById.getData().contains(item))
                    .collect(Collectors.toList());
            refundReqDTO.setRefundTypeEnum(RefundTypeEnum.PARTIAL_REFUND);
            // 3.4、设置过滤后具体要退款的车票子订单
            refundReqDTO.setRefundDetailReqDTOList(partialRefundPassengerDetails);
        } else if (RefundTypeEnum.FULL_REFUND.getType().equals(requestParam.getType())) {
            // 3、全部退款
            refundReqDTO.setRefundTypeEnum(RefundTypeEnum.FULL_REFUND);
            refundReqDTO.setRefundDetailReqDTOList(passengerDetails);
        }
        // 4、计算退款金额 因为是用分存储的，所以直接可以算
        if (CollectionUtil.isNotEmpty(passengerDetails)) {
            Integer partialRefundAmount = passengerDetails.stream()
                    .mapToInt(TicketOrderPassengerDetailRespDTO::getAmount)
                    .sum();
            refundReqDTO.setRefundAmount(partialRefundAmount);
        }
        refundReqDTO.setOrderSn(requestParam.getOrderSn());
        // 5、远程调用退款服务
        Result<RefundRespDTO> refundRespDTOResult = payRemoteService.commonRefund(refundReqDTO);
        if (!refundRespDTOResult.isSuccess() && Objects.isNull(refundRespDTOResult.getData())) {
            throw new ServiceException("车票订单退款失败");
        }
        return null; // 暂时返回空实体
    }

    private List<String> buildDepartureStationList(List<TicketListDTO> seatResults) {
        return seatResults.stream().map(TicketListDTO::getDeparture).distinct().collect(Collectors.toList());
    }

    private List<String> buildArrivalStationList(List<TicketListDTO> seatResults) {
        return seatResults.stream().map(TicketListDTO::getArrival).distinct().collect(Collectors.toList());
    }

    private List<Integer> buildSeatClassList(List<TicketListDTO> seatResults) {
        Set<Integer> resultSeatClassList = new HashSet<>();
        for (TicketListDTO each : seatResults) {
            for (SeatClassDTO item : each.getSeatClassList()) {
                resultSeatClassList.add(item.getType());
            }
        }
        return resultSeatClassList.stream().toList();
    }

    private List<Integer> buildTrainBrandList(List<TicketListDTO> seatResults) {
        Set<Integer> trainBrandSet = new HashSet<>();
        for (TicketListDTO each : seatResults) {
            if (StrUtil.isNotBlank(each.getTrainBrand())) {
                trainBrandSet.addAll(StrUtil.split(each.getTrainBrand(), ",").stream().map(Integer::parseInt).toList());
            }
        }
        return trainBrandSet.stream().toList();
    }

    // keypoint 之所以自己注入一个自己，就是因为要避免事务失效 直接用this调用本类方法，事务会失效的
    //  ，this是调用的原本对象的方法，但是事务是基于代理对象的，如果不用代理对象的方法，代理对象加强的内容全都没了，事务就没有生效
    // https://blog.csdn.net/belongtocode/article/details/133326343
    @Override
    public void run(String... args) throws Exception {
        ticketService = ApplicationContextHolder.getBean(TicketService.class);
    }
}
